package tk.mybatis.hot;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

/**
 * 刷新使用进程
 */
public class HotMapper {

	public static Log log = LogFactory.getLog(HotMapper.class);

	private HotConfiguration hotConfiguration;

	/**
	 * 上一次刷新时间
	 */
	private Long beforeTime = 0L;

	/**
	 * mapperLocations 地址
	 */
	private String mapperLocations;

	/**
	 * 延迟加载时间，单位秒
	 */
	private int delaySeconds = 5;

	/**
	 * 刷新间隔时间，单位秒
	 */
	private int sleepSeconds = 3;

	/**
	 * 是否开启刷新mapper
	 */
	private boolean enabled = false;

	/**
	 * 是否校验 mapper 文件,用于定位 mapper有效文件
	 */
	private boolean validation = false;

	// 资源文件查找
	private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

	// mapper 刷新 线程
	private Thread thread;

	public HotMapper(String mapperLocations, SqlSessionFactory sqlSessionFactory, int delaySeconds, int sleepSeconds,
			boolean enabled) {
		this(mapperLocations, sqlSessionFactory, enabled);
		this.delaySeconds = delaySeconds;
		this.sleepSeconds = sleepSeconds;
	}

	public HotMapper(String mapperLocations, SqlSessionFactory sqlSessionFactory, boolean enabled) {
		this.mapperLocations = mapperLocations;
		this.hotConfiguration = asHotConfiguration(sqlSessionFactory);
		this.enabled = enabled;
		this.beforeTime = System.currentTimeMillis();
	}

	public void stop() {
		if (thread != null) {
			thread.interrupt();
		}
	}

	public void start() {
		if (log.isDebugEnabled()) {
			log.debug("[delaySeconds] " + delaySeconds);
			log.debug("[sleepSeconds] " + sleepSeconds);
			log.debug("[mapperLocations] " + mapperLocations);
			log.debug("[configuration] " + hotConfiguration);
		}
		if (enabled) {
			Runnable hotRunnable = new Runnable() {
				@Override
				public void run() {
					HotMapper.this.refresh();
				}
			};
			thread = new Thread(hotRunnable);
			thread.start();
		}
	}

	public void refresh() {
		try {
			Thread.sleep(delaySeconds * 1000);
		} catch (InterruptedException e) {
			if (log.isDebugEnabled()) {
				log.debug("延迟加载 mapper 错误：" + e.getMessage());
			}
		}

		System.out.println("========= Enabled refresh mybatis mapper =========");

		while (true) {
			try {
				refresh(mapperLocations, beforeTime, hotConfiguration);
			} catch (Exception e) {
				e.printStackTrace();
			}

			try {
				Thread.sleep(sleepSeconds * 1000);
			} catch (InterruptedException e) {
				if (log.isDebugEnabled()) {
					log.debug("刷新 mapper 错误：" + e.getMessage());
				}
			}
		}
	}

	/**
	 * 执行刷新
	 * 
	 * @param mapperLocations 刷新目录
	 * @param beforeTime 上次刷新时间
	 * @throws IOException 解析异常
	 * @throws FileNotFoundException 文件未找到
	 */
	public void refresh(String mapperLocations, Long beforeTime, Configuration configuration) throws Exception {
		// 本次刷新时间
		Long refrehTime = System.currentTimeMillis();
		List<Resource> resources = getRefreshResource(mapperLocations, beforeTime, configuration);

		if (resources != null && resources.size() > 0) {
			log.debug("refresh files:" + resources.size());
			for (Resource mapperLocation : resources) {
				try {
					XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration,
							mapperLocation.toString(), configuration.getSqlFragments());
					xmlMapperBuilder.parse();
				} catch (Exception e) {
					throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
				} finally {
					ErrorContext.instance().reset();
				}

				if (log.isDebugEnabled()) {
					log.debug("Parsed mapper file: '" + mapperLocation + "'");
				}
			}

			// 如果刷新了文件，则修改刷新时间，否则不修改
			this.beforeTime = refrehTime;
		}
	}

	/**
	 * 获取需要刷新的文件列表
	 * 
	 * @param mapperLocations 目录
	 * @param beforeTime 上次刷新时间
	 * @return 刷新文件列表
	 */
	public List<Resource> getRefreshResource(String mapperLocations, Long beforeTime, Configuration configuration)
			throws Exception {
		Resource[] resources = getResourcePatternResolver().getResources(mapperLocations);
		List<Resource> refreshs = new ArrayList<Resource>();
		if (resources != null) {
			for (Resource resource : resources) {
				File file = resource.getFile();
				if (isXML(file) && check(file, beforeTime)) {
					// 不是 mapper 文件继续
					if (!isMapper(file, configuration)) {
						continue;
					}
					refreshs.add(resource);
				}
			}
		}

		return refreshs;
	}

	/**
	 * 是否是 xml 文件
	 */
	public boolean isXML(File file) {
		return file.getAbsolutePath().toLowerCase().endsWith(".xml");
	}

	/**
	 * 判断文件是否需要刷新
	 * 
	 * @param file 文件
	 * @param beforeTime 上次刷新时间
	 * @return 需要刷新返回 true，否则返回 false
	 */
	public boolean check(File file, Long beforeTime) {
		return file.lastModified() > beforeTime;
	}

	public boolean isMapper(File file, Configuration configuration) {
		// 不校验默认是 mapper 文件
		if (!validation) return true;
		try {
			new XPathParser(new FileInputStream(file), true, configuration.getVariables(), new XMLMapperEntityResolver());
			return true;
		} catch (Exception e) {
			return false;
		}
	}

	public void setResourcePatternResolver(ResourcePatternResolver resolver) {
		this.resourcePatternResolver = resolver;
	}

	public ResourcePatternResolver getResourcePatternResolver() {
		return resourcePatternResolver;
	}

	/**
	 * 强制转换 HotConfiguration
	 */
	private HotConfiguration asHotConfiguration(SqlSessionFactory sqlSessionFactory) {
		Configuration configuration = sqlSessionFactory.getConfiguration();
		if (configuration instanceof HotConfiguration) {//
			return (HotConfiguration) configuration;
		}
		throw new IllegalArgumentException("转换错误，请用  HotSqlSessionFactoryBuilder 创建 SqlSessionFactory 。class['"
				+ sqlSessionFactory.getConfiguration().getClass() + "']");
	}
}
